﻿/* 
 * Copyright (c) Intel Corporation
 * All rights reserved.
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * -- Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * -- Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * -- Neither the name of the Intel Corporation nor the names of its
 *    contributors may be used to endorse or promote products derived from
 *    this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 * PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE INTEL OR ITS
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Net;
using System.Web;
using System.Text;
using DotNetOpenAuth;
using DotNetOpenAuth.Messaging;
using DotNetOpenAuth.OpenId;
using DotNetOpenAuth.OpenId.Extensions.AttributeExchange;
using DotNetOpenAuth.OpenId.Extensions.SimpleRegistration;
using DotNetOpenAuth.OpenId.Provider;
using ExtensionLoader;
using ExtensionLoader.Config;
using HttpServer;
using OpenMetaverse;
using OpenMetaverse.StructuredData;
using CableBeachMessages;
using OpenSimDbLinq;

namespace WorldServer.Extensions
{
    public class OpenSimOpenIDProvider : IExtension<WorldServer>
    {
        const string LOGIN_FORM_PAGE = WorldServer.WEB_CONTENT_DIR + "openidproviderlogin.tpl";
        const string IDENTIFIER_PAGE = WorldServer.WEB_CONTENT_DIR + "identifier.tpl";
        const string IDENTIFIER_NOT_FOUND_PAGE = WorldServer.WEB_CONTENT_DIR + "identifiernotfound.tpl";

        /// <summary>A special attribute used to hold the avatar password hash pulled from the OpenSim database</summary>
        static readonly Uri AVATAR_PASSWORD_HASH = new Uri("http://opensimulator.org/attributes/avatarPasswordHash");
        static readonly Uri AVATAR_PASSWORD_SALT = new Uri("http://opensimulator.org/attributes/avatarPasswordSalt");

        WorldServer server;
        OpenSimDatabase userDatabase;
        StandardProviderApplicationStore openidStore;
        OpenIdProvider openidProvider;

        public OpenSimOpenIDProvider()
        {
        }

        public bool Start(WorldServer server)
        {
            this.server = server;

            #region Config Loading

            try
            {
                IConfig userdbConfig = server.ConfigFile.Configs["OpenSimUserDatabase"];
                userDatabase = OpenSimUtils.LoadDatabaseFromConfig(userdbConfig);

                try
                {
                    // DEBUG:
                    //userDatabase.Log = Console.Out;

                    // Run a query to select the the first user in the database to confirm
                    // that database access is working
                    var creationDateQuery = from user in userDatabase.Users select user;
                    var result = creationDateQuery.SingleOrDefault();

                    if (result != null)
                        Logger.Info("[OpenSimOpenIDProvider] Connected to user database");
                    else
                        Logger.Warn("[OpenSimOpenIDProvider] Connected to an empty user database");
                }
                catch (Exception ex)
                {
                    Logger.Error("[OpenSimOpenIDProvider] Failed to establish a database connection: " + ex.Message);
                    return false;
                }
            }
            catch (Exception)
            {
                Logger.Error("[OpenSimOpenIDProvider] Failed to load [OpenSimDatabase] section from " + WorldServer.CONFIG_FILE);
                return false;
            }

            #endregion Config Loading

            // Initialize the OpenID association store and provider
            openidStore = new StandardProviderApplicationStore();
            openidProvider = new OpenIdProvider(openidStore);

            // User identifier pages
            server.HttpServer.AddHandler("GET", null, @"^/users/", IdentifierPageHandler);
            // OpenID server
            server.HttpServer.AddHandler("GET", null, @"^/openid/server", OpenIDServerGetHandler);
            server.HttpServer.AddHandler("POST", null, @"^/openid/server", OpenIDServerPostHandler);

            return true;
        }

        public void Stop()
        {
            if (userDatabase != null)
                userDatabase.Dispose();
        }

        void IdentifierPageHandler(IHttpClientContext client, IHttpRequest request, IHttpResponse response)
        {
            // Make sure the site is being visited through the hostname that will be used in our OpenID realm.
            // Otherwise, the OpenID login will fail
            if (request.Uri.Authority != server.HttpUri.Authority)
            {
                Uri redirect = new Uri(server.HttpUri, request.Uri.PathAndQuery);

                Logger.Info("[OpenSimOpenIDProvider] Redirecting from " + request.Uri + " to " + redirect);
                response.Status = HttpStatusCode.Found;
                response.AddHeader("Location", redirect.ToString());
                return;
            }

            UUID avatarID;
            Dictionary<Uri, OSD> attributes;

            if (TryGetProfile(request.Uri, out avatarID, out attributes))
            {
                if (request.Uri.AbsolutePath.EndsWith(";xrd"))
                {
                    Uri identity = new Uri(request.Uri.ToString().Replace(";xrd", String.Empty));

                    // Fetch the list of services requirements for this identity
                    ServiceCollection services = new ServiceCollection();
                    server.ServiceProvider.GetServices(identity, false, ref services);

                    // Create an XRD document from the identity URL and known services
                    XrdDocument xrd = new XrdDocument(request.Uri.ToString());

                    xrd.Links.Add(new XrdLink(new Uri("http://specs.openid.net/auth"), null, new XrdUri(identity)));
                    foreach (KeyValuePair<Uri, Service> entry in services)
                    {
                        if (entry.Value.XrdDocument != null)
                            xrd.Links.Add(new XrdLink(entry.Key, null, new XrdUri(entry.Value.XrdDocument)));
                    }

                    SendXrdDocument(response, xrd);
                }
                else
                {
                    SendIdentifierTemplate(response, avatarID, GetStringAttribute(attributes, AvatarAttributes.FIRST_NAME), GetStringAttribute(attributes, AvatarAttributes.LAST_NAME));
                }
            }
            else
            {
                SendIdentifierNotFoundTemplate(response);
            }
        }

        void OpenIDServerGetHandler(IHttpClientContext client, IHttpRequest request, IHttpResponse response)
        {
            IRequest openidRequest = openidProvider.GetRequest(OpenAuthHelper.GetRequestInfo(request));

            if (openidRequest != null)
            {
                if (openidRequest is IAuthenticationRequest)
                {
                    IAuthenticationRequest authRequest = (IAuthenticationRequest)openidRequest;

                    // Check for cancellations
                    if (request.QueryString.Contains("cancel"))
                    {
                        Logger.Warn("[OpenSimOpenIDProvider] Request to " + request.Uri + " contained a cancel argument, sending a negative assertion");
                        authRequest.IsAuthenticated = false;
                        OpenAuthHelper.OpenAuthResponseToHttp(response, openidProvider.PrepareResponse(openidRequest));
                        return;
                    }

                    UUID avatarID;
                    Dictionary<Uri, OSD> attributes;
                    if (TryGetProfile(new Uri(authRequest.ClaimedIdentifier.ToString()), out avatarID, out attributes))
                    {
                        // Authentication was requested
                        BuildLoginForm(request, response, null, authRequest, avatarID, attributes);
                    }
                    else
                    {
                        // Cannot find an avatar matching the claimed identifier
                        Logger.Warn("[OpenSimOpenIDProvider] (GET) Could not locate an avatar identity from the claimed identitifer " +
                            authRequest.ClaimedIdentifier.ToString());
                        authRequest.IsAuthenticated = false;
                    }
                }

                if (openidRequest.IsResponseReady)
                {
                    OpenAuthHelper.OpenAuthResponseToHttp(response, openidProvider.PrepareResponse(openidRequest));
                }
            }
        }

        void OpenIDServerPostHandler(IHttpClientContext client, IHttpRequest request, IHttpResponse response)
        {
            IRequest openidRequest = openidProvider.GetRequest(OpenAuthHelper.GetRequestInfo(request));

            if (openidRequest != null)
            {
                if (openidRequest is IAuthenticationRequest)
                {
                    IAuthenticationRequest authRequest = (IAuthenticationRequest)openidRequest;
                    ClaimsRequest claimsRequest = openidRequest.GetExtension<ClaimsRequest>();

                    UUID avatarID;
                    Dictionary<Uri, OSD> attributes;
                    if (TryGetProfile(new Uri(authRequest.ClaimedIdentifier.ToString()), out avatarID, out attributes))
                    {
                        // Get the password from the POST data
                        NameValueCollection postData = HttpUtility.ParseQueryString(new StreamReader(request.Body).ReadToEnd());
                        string[] passwordValues = postData.GetValues("password");

                        if (passwordValues != null && passwordValues.Length == 1)
                        {
                            // Test the password
                            string passwordHash = Utils.MD5String(Utils.MD5String(passwordValues[0]) + ":" + GetStringAttribute(attributes, AVATAR_PASSWORD_SALT));
                            string dbPasswordHash = GetStringAttribute(attributes, AVATAR_PASSWORD_HASH);
                            authRequest.IsAuthenticated = passwordHash.Equals(dbPasswordHash, StringComparison.InvariantCultureIgnoreCase);
                            Logger.Info("[OpenSimOpenIDProvider] Password match success for + " + avatarID + ": " + authRequest.IsAuthenticated);

                            if (authRequest.IsAuthenticated.Value && claimsRequest != null)
                            {
                                // Fill in a few Simple Registration values if there was a request for SREG data
                                ClaimsResponse claimsResponse = claimsRequest.CreateResponse();

                                string email = GetStringAttribute(attributes, AvatarAttributes.EMAIL);
                                if (!String.IsNullOrEmpty(email))
                                    claimsResponse.Email = email;
                                string firstName = GetStringAttribute(attributes, AvatarAttributes.FIRST_NAME);
                                string lastName = GetStringAttribute(attributes, AvatarAttributes.LAST_NAME);
                                if (!String.IsNullOrEmpty(firstName) || !String.IsNullOrEmpty(lastName))
                                    claimsResponse.FullName = (firstName + " " + lastName).Trim();

                                Logger.Debug("[OpenSimOpenIDProvider] Appending SREG values to the positive assertion response");
                                authRequest.AddResponseExtension(claimsResponse);
                            }
                        }
                        else
                        {
                            Logger.Warn("[OpenSimOpenIDProvider] Got a POST to /openid/server with a missing or invalid password field");
                            BuildLoginForm(request, response, postData, authRequest, avatarID, attributes);
                            return;
                        }
                    }
                    else
                    {
                        // Cannot find an avatar matching the claimed identifier
                        Logger.Warn("[OpenSimOpenIDProvider] (POST) Could not locate an avatar identity from the claimed identitifer " +
                            authRequest.ClaimedIdentifier.ToString());
                        authRequest.IsAuthenticated = false;
                    }
                }

                if (openidRequest.IsResponseReady)
                {
                    OpenAuthHelper.OpenAuthResponseToHttp(response, openidProvider.PrepareResponse(openidRequest));
                }
            }
        }

        void BuildLoginForm(IHttpRequest request, IHttpResponse response, NameValueCollection postData, IAuthenticationRequest authRequest,
            UUID avatarID, Dictionary<Uri, OSD> attributes)
        {
            // Convert all of the query parameters to form input fields
            StringBuilder formHiddenFields = new StringBuilder();
            Dictionary<string, string> queryValues = OpenAuthHelper.QueryStringToDictionary(request.Uri.Query);
            foreach (KeyValuePair<string, string> entry in queryValues)
                formHiddenFields.AppendFormat("<input name=\"{0}\" type=\"hidden\" value=\"{1}\"/>", entry.Key, entry.Value);

            if (postData != null)
            {
                // Convert all of the POST parameters to form input fields
                foreach (string key in postData.Keys)
                    formHiddenFields.AppendFormat("<input name=\"{0}\" type=\"hidden\" value=\"{1}\"/>", key, postData[key]);
            }

            // Sent the client a login form
            SendLoginFormTemplate(response, avatarID, authRequest.ClaimedIdentifier, authRequest.Realm, formHiddenFields.ToString(),
                GetStringAttribute(attributes, AvatarAttributes.FIRST_NAME),
                GetStringAttribute(attributes, AvatarAttributes.LAST_NAME), new Uri(server.HttpUri, "/openid/server"));
        }

        void SendLoginFormTemplate(IHttpResponse response, UUID avatarID, Identifier identifier, Realm realm, string formHiddenFields,
            string firstName, string lastName, Uri formPostUrl)
        {
            IDictionary<string, object> variables = new Dictionary<string, object>();
            variables["avatar_id"] = avatarID.ToString();
            variables["identifier"] = identifier.ToString();
            variables["realm"] = realm.ToString();
            variables["form_hidden_fields"] = formHiddenFields;
            variables["first_name"] = (firstName != null) ? firstName : String.Empty;
            variables["last_name"] = (lastName != null) ? lastName : String.Empty;
            variables["form_post_url"] = formPostUrl.ToString();

            try
            {
                string output = server.HttpTemplates.Render(LOGIN_FORM_PAGE, variables);
                byte[] data = Encoding.UTF8.GetBytes(output);
                response.ContentLength = data.Length;
                response.ContentType = "text/html";
                response.Body.Write(data, 0, data.Length);
            }
            catch (Exception ex)
            {
                Logger.Error("[OpenSimOpenIDProvider] Failed to render " + LOGIN_FORM_PAGE + " template: " + ex.Message);
            }
        }

        void SendIdentifierTemplate(IHttpResponse response, UUID avatarID, string firstName, string lastName)
        {
            Uri xrdLink = new Uri(server.HttpUri, "/users/" + firstName + "." + lastName + ";xrd");

            IDictionary<string, object> variables = new Dictionary<string, object>();
            variables["avatar_id"] = avatarID.ToString();
            variables["first_name"] = (firstName != null) ? firstName : String.Empty;
            variables["last_name"] = (lastName != null) ? lastName : String.Empty;
            variables["openid_server_url"] = new Uri(server.HttpUri, "/openid/server").ToString();
            variables["xrd_url"] = xrdLink;

            try
            {
                string output = server.HttpTemplates.Render(IDENTIFIER_PAGE, variables);
                byte[] data = Encoding.UTF8.GetBytes(output);
                response.AddHeader("link", String.Format("<{0}>; rel=\"describedby\"; type=\"application/xrd+xml\"", xrdLink));
                response.ContentLength = data.Length;
                response.ContentType = "text/html";
                response.Body.Write(data, 0, data.Length);
            }
            catch (Exception ex)
            {
                Logger.Error("[OpenSimOpenIDProvider] Failed to render " + IDENTIFIER_PAGE + " template: " + ex.Message);
            }
        }

        void SendIdentifierNotFoundTemplate(IHttpResponse response)
        {
            IDictionary<string, object> variables = new Dictionary<string, object>();

            try
            {
                string output = server.HttpTemplates.Render(IDENTIFIER_NOT_FOUND_PAGE, variables);
                byte[] data = Encoding.UTF8.GetBytes(output);
                response.Status = HttpStatusCode.NotFound;
                response.ContentLength = data.Length;
                response.ContentType = "text/html";
                response.Body.Write(data, 0, data.Length);
            }
            catch (Exception ex)
            {
                Logger.Error("[OpenSimOpenIDProvider] Failed to render " + IDENTIFIER_NOT_FOUND_PAGE + " template: " + ex.Message);
            }
        }

        void SendXrdDocument(IHttpResponse response, XrdDocument xrd)
        {
            // Send the document to the client
            byte[] data = Encoding.UTF8.GetBytes(XrdParser.WriteXrd(xrd));
            response.ContentLength = data.Length;
            response.ContentType = "application/xrd+xml";
            response.Body.Write(data, 0, data.Length);
        }

        bool TryGetProfile(Uri requestUrl, out UUID avatarID, out Dictionary<Uri, OSD> attributes)
        {
            if (requestUrl.Segments.Length == 3 && requestUrl.Segments[1] == "users/")
            {
                // Parse the avatar name from the path
                string username = requestUrl.Segments[requestUrl.Segments.Length - 1];

                // Look for a semi-colon and remove any characters after it to handle ";xrd" at the end of the URL
                int semicolonPos = username.LastIndexOf(';');
                if (semicolonPos != -1)
                    username = username.Remove(semicolonPos);

                string[] name = username.Split('.');

                if (name.Length == 2)
                {
                    var query = from user in userDatabase.Users
                                where user.UserName == name[0] && user.LastName == name[1].TrimEnd('/')
                                select user;
                    Users dbUser = query.SingleOrDefault();

                    if (dbUser != null)
                    {
                        attributes = new Dictionary<Uri, OSD>();
                        DatabaseRowToAttributes(dbUser, ref attributes, out avatarID);
                        return true;
                    }
                }
            }

            avatarID = UUID.Zero;
            attributes = null;
            return false;
        }

        void DatabaseRowToAttributes(Users dbUser, ref Dictionary<Uri, OSD> attributes, out UUID avatarID)
        {
            UUID.TryParse(dbUser.UUID, out avatarID);
            Vector3 homeLookAt = new Vector3(dbUser.HomeLookAtX, dbUser.HomeLookAtY, dbUser.HomeLookAtZ);
            Vector3 homePosition = new Vector3(dbUser.HomeLocationX, dbUser.HomeLocationY, dbUser.HomeLocationZ);
            uint homeRegionX, homeRegionY;
            Utils.LongToUInts((ulong)dbUser.HomeRegion, out homeRegionX, out homeRegionY);
            homeRegionX /= 256;
            homeRegionY /= 256;

            attributes[AvatarAttributes.AVATAR_ID] = OSD.FromUUID(avatarID);

            if (!attributes.ContainsKey(AvatarAttributes.BIOGRAPHY)) attributes[AvatarAttributes.BIOGRAPHY] = OSD.FromString(dbUser.ProfileAboutText);
            if (!attributes.ContainsKey(AvatarAttributes.BIRTH_DATE)) attributes[AvatarAttributes.BIRTH_DATE] = OSD.FromDate(Utils.UnixTimeToDateTime(dbUser.Created));
            if (!attributes.ContainsKey(AvatarAttributes.CAN_DO)) attributes[AvatarAttributes.CAN_DO] = OSD.FromUInteger(dbUser.ProfileCanDoMask);
            if (!attributes.ContainsKey(AvatarAttributes.CUSTOM_TYPE)) attributes[AvatarAttributes.CUSTOM_TYPE] = OSD.FromString(dbUser.CustomType);
            if (!attributes.ContainsKey(AvatarAttributes.EMAIL)) attributes[AvatarAttributes.EMAIL] = OSD.FromString(dbUser.Email);
            if (!attributes.ContainsKey(AvatarAttributes.FIRST_LIFE_BIOGRAPHY)) attributes[AvatarAttributes.FIRST_LIFE_BIOGRAPHY] = OSD.FromString(dbUser.ProfileFirstText);
            if (!attributes.ContainsKey(AvatarAttributes.FIRST_LIFE_IMAGE_ID)) attributes[AvatarAttributes.FIRST_LIFE_IMAGE_ID] = OSD.FromString(dbUser.ProfileFirstImage);
            if (!attributes.ContainsKey(AvatarAttributes.FIRST_NAME)) attributes[AvatarAttributes.FIRST_NAME] = OSD.FromString(dbUser.UserName);
            if (!attributes.ContainsKey(AvatarAttributes.GOD_LEVEL)) attributes[AvatarAttributes.GOD_LEVEL] = OSD.FromInteger(dbUser.GodLevel);
            if (!attributes.ContainsKey(AvatarAttributes.HOME_LOOKAT)) attributes[AvatarAttributes.HOME_LOOKAT] = OSD.FromVector3(homeLookAt);
            if (!attributes.ContainsKey(AvatarAttributes.HOME_POSITION)) attributes[AvatarAttributes.HOME_POSITION] = OSD.FromVector3(homePosition);
            if (!attributes.ContainsKey(AvatarAttributes.HOME_REGION_ID)) attributes[AvatarAttributes.HOME_REGION_ID] = OSD.FromString(dbUser.HomeRegionID);
            if (!attributes.ContainsKey(AvatarAttributes.HOME_REGION_X)) attributes[AvatarAttributes.HOME_REGION_X] = OSD.FromUInteger(homeRegionX);
            if (!attributes.ContainsKey(AvatarAttributes.HOME_REGION_Y)) attributes[AvatarAttributes.HOME_REGION_Y] = OSD.FromUInteger(homeRegionY);
            if (!attributes.ContainsKey(AvatarAttributes.IMAGE_ID)) attributes[AvatarAttributes.IMAGE_ID] = OSD.FromString(dbUser.ProfileImage);
            if (!attributes.ContainsKey(AvatarAttributes.LAST_NAME)) attributes[AvatarAttributes.LAST_NAME] = OSD.FromString(dbUser.LastName);
            if (!attributes.ContainsKey(AvatarAttributes.PARTNER_ID)) attributes[AvatarAttributes.PARTNER_ID] = OSD.FromString(dbUser.Partner);
            if (!attributes.ContainsKey(AvatarAttributes.USER_FLAGS)) attributes[AvatarAttributes.USER_FLAGS] = OSD.FromInteger(dbUser.UserFlags);
            if (!attributes.ContainsKey(AvatarAttributes.WANT_DO)) attributes[AvatarAttributes.WANT_DO] = OSD.FromInteger(dbUser.ProfileWantDoMask);

            // Fetch the appearance for this avatar
            AvatarAppearance dbAppearance = userDatabase.AvatarAppearance.SingleOrDefault(user => user.Owner == dbUser.UUID);
            if (dbAppearance != null)
            {
                if (!attributes.ContainsKey(AvatarAttributes.EYES_ITEM)) attributes[AvatarAttributes.EYES_ITEM] = OSD.FromString(dbAppearance.EyesItem);
                if (!attributes.ContainsKey(AvatarAttributes.GLOVES_ITEM)) attributes[AvatarAttributes.GLOVES_ITEM] = OSD.FromString(dbAppearance.GlovesItem);
                if (!attributes.ContainsKey(AvatarAttributes.HAIR_ITEM)) attributes[AvatarAttributes.HAIR_ITEM] = OSD.FromString(dbAppearance.HairItem);
                if (!attributes.ContainsKey(AvatarAttributes.JACKET_ITEM)) attributes[AvatarAttributes.JACKET_ITEM] = OSD.FromString(dbAppearance.JacketItem);
                if (!attributes.ContainsKey(AvatarAttributes.PANTS_ITEM)) attributes[AvatarAttributes.PANTS_ITEM] = OSD.FromString(dbAppearance.PantsItem);
                if (!attributes.ContainsKey(AvatarAttributes.SHAPE_ITEM)) attributes[AvatarAttributes.SHAPE_ITEM] = OSD.FromString(dbAppearance.BodyItem);
                if (!attributes.ContainsKey(AvatarAttributes.SHIRT_ITEM)) attributes[AvatarAttributes.SHIRT_ITEM] = OSD.FromString(dbAppearance.ShirtItem);
                if (!attributes.ContainsKey(AvatarAttributes.SHOES_ITEM)) attributes[AvatarAttributes.SHOES_ITEM] = OSD.FromString(dbAppearance.ShoesItem);
                if (!attributes.ContainsKey(AvatarAttributes.SKIN_ITEM)) attributes[AvatarAttributes.SKIN_ITEM] = OSD.FromString(dbAppearance.SkinItem);
                if (!attributes.ContainsKey(AvatarAttributes.SKIRT_ITEM)) attributes[AvatarAttributes.SKIRT_ITEM] = OSD.FromString(dbAppearance.SkirtItem);
                if (!attributes.ContainsKey(AvatarAttributes.SOCKS_ITEM)) attributes[AvatarAttributes.SOCKS_ITEM] = OSD.FromString(dbAppearance.SocksItem);
                if (!attributes.ContainsKey(AvatarAttributes.UNDERPANTS_ITEM)) attributes[AvatarAttributes.UNDERPANTS_ITEM] = OSD.FromString(dbAppearance.UnderpantsItem);
                if (!attributes.ContainsKey(AvatarAttributes.UNDERSHIRT_ITEM)) attributes[AvatarAttributes.UNDERSHIRT_ITEM] = OSD.FromString(dbAppearance.UndershirtItem);

                if (!attributes.ContainsKey(AvatarAttributes.EYES_ASSET)) attributes[AvatarAttributes.EYES_ASSET] = OSD.FromString(dbAppearance.EyesAsset);
                if (!attributes.ContainsKey(AvatarAttributes.GLOVES_ASSET)) attributes[AvatarAttributes.GLOVES_ASSET] = OSD.FromString(dbAppearance.GlovesAsset);
                if (!attributes.ContainsKey(AvatarAttributes.HAIR_ASSET)) attributes[AvatarAttributes.HAIR_ASSET] = OSD.FromString(dbAppearance.HairAsset);
                if (!attributes.ContainsKey(AvatarAttributes.JACKET_ASSET)) attributes[AvatarAttributes.JACKET_ASSET] = OSD.FromString(dbAppearance.JacketAsset);
                if (!attributes.ContainsKey(AvatarAttributes.PANTS_ASSET)) attributes[AvatarAttributes.PANTS_ASSET] = OSD.FromString(dbAppearance.PantsAsset);
                if (!attributes.ContainsKey(AvatarAttributes.SHAPE_ASSET)) attributes[AvatarAttributes.SHAPE_ASSET] = OSD.FromString(dbAppearance.BodyAsset);
                if (!attributes.ContainsKey(AvatarAttributes.SHIRT_ASSET)) attributes[AvatarAttributes.SHIRT_ASSET] = OSD.FromString(dbAppearance.ShirtAsset);
                if (!attributes.ContainsKey(AvatarAttributes.SHOES_ASSET)) attributes[AvatarAttributes.SHOES_ASSET] = OSD.FromString(dbAppearance.ShoesAsset);
                if (!attributes.ContainsKey(AvatarAttributes.SKIN_ASSET)) attributes[AvatarAttributes.SKIN_ASSET] = OSD.FromString(dbAppearance.SkinAsset);
                if (!attributes.ContainsKey(AvatarAttributes.SKIRT_ASSET)) attributes[AvatarAttributes.SKIRT_ASSET] = OSD.FromString(dbAppearance.SkirtAsset);
                if (!attributes.ContainsKey(AvatarAttributes.SOCKS_ASSET)) attributes[AvatarAttributes.SOCKS_ASSET] = OSD.FromString(dbAppearance.SocksAsset);
                if (!attributes.ContainsKey(AvatarAttributes.UNDERPANTS_ASSET)) attributes[AvatarAttributes.UNDERPANTS_ASSET] = OSD.FromString(dbAppearance.UnderpantsAsset);
                if (!attributes.ContainsKey(AvatarAttributes.UNDERSHIRT_ASSET)) attributes[AvatarAttributes.UNDERSHIRT_ASSET] = OSD.FromString(dbAppearance.UndershirtAsset);

                if (!attributes.ContainsKey(AvatarAttributes.TEXTURE_ENTRY)) attributes[AvatarAttributes.TEXTURE_ENTRY] = OSD.FromBinary(dbAppearance.Texture);
                if (!attributes.ContainsKey(AvatarAttributes.VISUAL_PARAMS)) attributes[AvatarAttributes.VISUAL_PARAMS] = OSD.FromBinary(dbAppearance.VisualParams);
                if (!attributes.ContainsKey(AvatarAttributes.AVATAR_HEIGHT)) attributes[AvatarAttributes.AVATAR_HEIGHT] = OSD.FromReal(dbAppearance.AvatarHeight);
            }

            // Fetch the last starting location for this avatar
            Agents dbAgent = userDatabase.Agents.SingleOrDefault(user => user.UUID == dbUser.UUID);
            if (dbAgent != null)
            {
                Vector3 position, lookAt;
                if (!Vector3.TryParse(dbAgent.CurrentPos, out position))
                    position = new Vector3(128f, 128f, 100f);
                if (!Vector3.TryParse(dbAgent.CurrentLookAt, out lookAt))
                    lookAt = Vector3.UnitZ;

                uint regionX, regionY;
                Utils.LongToUInts((ulong)dbAgent.CurrentHandle, out regionX, out regionY);
                regionX /= 256;
                regionY /= 256;

                if (!attributes.ContainsKey(AvatarAttributes.LAST_LOOKAT)) attributes[AvatarAttributes.LAST_LOOKAT] = OSD.FromVector3(lookAt);
                if (!attributes.ContainsKey(AvatarAttributes.LAST_POSITION)) attributes[AvatarAttributes.LAST_POSITION] = OSD.FromVector3(position);
                if (!attributes.ContainsKey(AvatarAttributes.LAST_REGION_ID)) attributes[AvatarAttributes.LAST_REGION_ID] = OSD.FromString(dbAgent.CurrentRegion);
                if (!attributes.ContainsKey(AvatarAttributes.LAST_REGION_X)) attributes[AvatarAttributes.LAST_REGION_X] = OSD.FromUInteger(regionX);
                if (!attributes.ContainsKey(AvatarAttributes.LAST_REGION_Y)) attributes[AvatarAttributes.LAST_REGION_Y] = OSD.FromUInteger(regionY);
            }

            // Specific to this module: Fetch the avatar password hash and salt
            attributes[AVATAR_PASSWORD_HASH] = OSD.FromString(dbUser.PasswordHash);
            attributes[AVATAR_PASSWORD_SALT] = OSD.FromString(dbUser.PasswordSalt);
        }

        private static string GetStringAttribute(Dictionary<Uri, OSD> attributes, Uri attribute)
        {
            OSD attributeData;
            if (attributes.TryGetValue(attribute, out attributeData))
                return attributeData.AsString();
            return String.Empty;
        }
    }
}
